package config

import (
	"bytes"
	"os"
	"path/filepath"
	"testing"

	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	sdk "github.com/cosmos/cosmos-sdk/types"
)

func TestDefaultConfig(t *testing.T) {
	cfg := DefaultConfig()
	require.True(t, cfg.GetMinGasPrices().IsZero())
}

func TestGetAndSetMinimumGas(t *testing.T) {
	cfg := DefaultConfig()

	input := sdk.DecCoins{sdk.NewInt64DecCoin("foo", 5)}
	cfg.SetMinGasPrices(input)
	require.Equal(t, "5.000000000000000000foo", cfg.MinGasPrices)
	require.EqualValues(t, cfg.GetMinGasPrices(), input)

	input = sdk.DecCoins{sdk.NewInt64DecCoin("bar", 1), sdk.NewInt64DecCoin("foo", 5)}
	cfg.SetMinGasPrices(input)
	require.Equal(t, "1.000000000000000000bar,5.000000000000000000foo", cfg.MinGasPrices)
	require.EqualValues(t, cfg.GetMinGasPrices(), input)
}

func TestIndexEventsMarshalling(t *testing.T) {
	expectedIn := `index-events = ["key1", "key2", ]` + "\n"
	cfg := DefaultConfig()
	cfg.IndexEvents = []string{"key1", "key2"}
	var buffer bytes.Buffer

	err := configTemplate.Execute(&buffer, cfg)
	require.NoError(t, err, "executing template")
	actual := buffer.String()
	require.Contains(t, actual, expectedIn, "config file contents")
}

func TestStreamingConfig(t *testing.T) {
	cfg := Config{
		Streaming: StreamingConfig{
			ABCI: ABCIListenerConfig{
				Keys:          []string{"one", "two"},
				Plugin:        "plugin-A",
				StopNodeOnErr: false,
			},
		},
	}

	testDir := t.TempDir()
	cfgFile := filepath.Join(testDir, "app.toml")
	WriteConfigFile(cfgFile, &cfg)

	cfgFileBz, err := os.ReadFile(cfgFile)
	require.NoError(t, err, "reading %s", cfgFile)
	cfgFileContents := string(cfgFileBz)
	t.Logf("Config file contents: %s:\n%s", cfgFile, cfgFileContents)

	expectedLines := []string{
		`keys = ["one", "two", ]`,
		`plugin = "plugin-A"`,
		`stop-node-on-err = false`,
	}

	for _, line := range expectedLines {
		assert.Contains(t, cfgFileContents, line+"\n", "config file contents")
	}

	vpr := viper.New()
	vpr.SetConfigFile(cfgFile)
	err = vpr.ReadInConfig()
	require.NoError(t, err, "reading config file into viper")

	var actual Config
	err = vpr.Unmarshal(&actual)
	require.NoError(t, err, "vpr.Unmarshal")

	assert.Equal(t, cfg.Streaming, actual.Streaming, "Streaming")
}

func TestParseStreaming(t *testing.T) {
	expectedKeys := `keys = ["*", ]` + "\n"
	expectedPlugin := `plugin = "abci_v1"` + "\n"
	expectedStopNodeOnErr := `stop-node-on-err = true` + "\n"

	cfg := DefaultConfig()
	cfg.Streaming.ABCI.Keys = []string{"*"}
	cfg.Streaming.ABCI.Plugin = "abci_v1"
	cfg.Streaming.ABCI.StopNodeOnErr = true

	var buffer bytes.Buffer
	err := configTemplate.Execute(&buffer, cfg)
	require.NoError(t, err, "executing template")
	actual := buffer.String()
	require.Contains(t, actual, expectedKeys, "config file contents")
	require.Contains(t, actual, expectedPlugin, "config file contents")
	require.Contains(t, actual, expectedStopNodeOnErr, "config file contents")
}

func TestReadConfig(t *testing.T) {
	cfg := DefaultConfig()
	tmpFile := filepath.Join(t.TempDir(), "config")
	WriteConfigFile(tmpFile, cfg)

	v := viper.New()
	otherCfg, err := GetConfig(v)
	require.NoError(t, err)

	require.Equal(t, *cfg, otherCfg)
}

func TestIndexEventsWriteRead(t *testing.T) {
	expected := []string{"key3", "key4"}

	// Create config with two IndexEvents entries, and write it to a file.
	confFile := filepath.Join(t.TempDir(), "app.toml")
	conf := DefaultConfig()
	conf.IndexEvents = expected

	WriteConfigFile(confFile, conf)

	// read the file into Viper
	vpr := viper.New()
	vpr.SetConfigFile(confFile)

	err := vpr.ReadInConfig()
	require.NoError(t, err, "reading config file into viper")

	// Check that the raw viper value is correct.
	actualRaw := vpr.GetStringSlice("index-events")
	require.Equal(t, expected, actualRaw, "viper's index events")

	// Check that it is parsed into the config correctly.
	cfg, perr := ParseConfig(vpr)
	require.NoError(t, perr, "parsing config")

	actual := cfg.IndexEvents
	require.Equal(t, expected, actual, "config value")
}

func TestGlobalLabelsEventsMarshalling(t *testing.T) {
	expectedIn := `global-labels = [
  ["labelname1", "labelvalue1"],
  ["labelname2", "labelvalue2"],
]`
	cfg := DefaultConfig()
	cfg.Telemetry.GlobalLabels = [][]string{{"labelname1", "labelvalue1"}, {"labelname2", "labelvalue2"}}
	var buffer bytes.Buffer

	err := configTemplate.Execute(&buffer, cfg)
	require.NoError(t, err, "executing template")
	actual := buffer.String()
	require.Contains(t, actual, expectedIn, "config file contents")
}

func TestGlobalLabelsWriteRead(t *testing.T) {
	expected := [][]string{{"labelname3", "labelvalue3"}, {"labelname4", "labelvalue4"}}
	expectedRaw := make([]any, len(expected))
	for i, exp := range expected {
		pair := make([]any, len(exp))
		for j, s := range exp {
			pair[j] = s
		}
		expectedRaw[i] = pair
	}

	// Create config with two GlobalLabels entries, and write it to a file.
	confFile := filepath.Join(t.TempDir(), "app.toml")
	conf := DefaultConfig()
	conf.Telemetry.GlobalLabels = expected
	WriteConfigFile(confFile, conf)

	// Read that file into viper.
	vpr := viper.New()
	vpr.SetConfigFile(confFile)
	rerr := vpr.ReadInConfig()
	require.NoError(t, rerr, "reading config file into viper")
	// Check that the raw viper value is correct.
	actualRaw := vpr.Get("telemetry.global-labels")
	require.Equal(t, expectedRaw, actualRaw, "viper value")
	// Check that it is parsed into the config correctly.
	cfg, perr := ParseConfig(vpr)
	require.NoError(t, perr, "parsing config")
	actual := cfg.Telemetry.GlobalLabels
	require.Equal(t, expected, actual, "config value")
}

func TestSetConfigTemplate(t *testing.T) {
	conf := DefaultConfig()
	var initBuffer, setBuffer bytes.Buffer

	// Use the configTemplate defined during init() to create a config string.
	ierr := configTemplate.Execute(&initBuffer, conf)
	require.NoError(t, ierr, "initial configTemplate.Execute")
	expected := initBuffer.String()

	// Set the template to the default one.
	initTmpl := configTemplate
	require.NotPanics(t, func() {
		SetConfigTemplate(DefaultConfigTemplate)
	}, "SetConfigTemplate")
	setTmpl := configTemplate
	require.NotSame(t, initTmpl, setTmpl, "configTemplate after set")

	// Create the string again and make sure it's the same.
	serr := configTemplate.Execute(&setBuffer, conf)
	require.NoError(t, serr, "after SetConfigTemplate, configTemplate.Execute")
	actual := setBuffer.String()
	require.Equal(t, expected, actual, "resulting config strings")
}

func TestAppConfig(t *testing.T) {
	appConfigFile := filepath.Join(t.TempDir(), "app.toml")
	defer func() {
		_ = os.Remove(appConfigFile)
	}()

	defAppConfig := DefaultConfig()
	SetConfigTemplate(DefaultConfigTemplate)
	WriteConfigFile(appConfigFile, defAppConfig)

	v := viper.New()
	v.SetConfigFile(appConfigFile)
	require.NoError(t, v.ReadInConfig())
	appCfg := new(Config)
	require.NoError(t, v.Unmarshal(appCfg))
	require.EqualValues(t, appCfg, defAppConfig)
}

func TestGetConfig_HistoricalGRPCAddressBlockRange(t *testing.T) {
	tests := []struct {
		name        string
		setupViper  func(*viper.Viper)
		expectError bool
		errorMsg    string
		validate    func(*testing.T, Config)
	}{
		{
			name: "valid single historical grpc address",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [0, 1000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{0, 1000}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists, "Block range [0, 1000] should exist")
				require.Equal(t, "localhost:9091", address)
			},
		},
		{
			name: "valid multiple historical grpc addresses",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range",
					`{"localhost:9091": [0, 1000], "localhost:9092": [1001, 2000], "localhost:9093": [2001, 3000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 3)
				testCases := []struct {
					blockRange BlockRange
					address    string
				}{
					{BlockRange{0, 1000}, "localhost:9091"},
					{BlockRange{1001, 2000}, "localhost:9092"},
					{BlockRange{2001, 3000}, "localhost:9093"},
				}
				for _, tc := range testCases {
					address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[tc.blockRange]
					require.True(t, exists, "Block range %v should exist", tc.blockRange)
					require.Equal(t, tc.address, address)
				}
			},
		},
		{
			name: "empty configuration",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", "")
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Nil(t, cfg.GRPC.HistoricalGRPCAddressBlockRange)
			},
		},
		{
			name:        "no configuration set",
			setupViper:  func(v *viper.Viper) {},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Nil(t, cfg.GRPC.HistoricalGRPCAddressBlockRange)
			},
		},
		{
			name: "invalid JSON format",
			setupViper: func(v *viper.Viper) {
				// missing closing brace
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [0, 1000]`)
			},
			expectError: true,
			errorMsg:    "failed to parse historical-grpc-address-block-range as JSON",
		},
		{
			name: "negative start block",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [-1, 1000]}`)
			},
			expectError: true,
			errorMsg:    "block numbers cannot be negative",
		},
		{
			name: "negative end block",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [0, -100]}`)
			},
			expectError: true,
			errorMsg:    "block numbers cannot be negative",
		},
		{
			name: "start block greater than end block",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [1000, 500]}`)
			},
			expectError: true,
			errorMsg:    "start block must be <= end block",
		},
		{
			name: "single block range (start equals end)",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [1000, 1000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{1000, 1000}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists)
				require.Equal(t, "localhost:9091", address)
			},
		},
		{
			name: "zero to zero range",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [0, 0]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{0, 0}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists)
				require.Equal(t, "localhost:9091", address)
			},
		},
		{
			name: "large block numbers",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [1000000, 2000000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{1000000, 2000000}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists)
				require.Equal(t, "localhost:9091", address)
			},
		},
		{
			name: "invalid array length (too few elements)",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [100]}`)
			},
			expectError: true,
			errorMsg:    "start block must be <= end block",
		},
		{
			name: "invalid array length (too many elements)",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"localhost:9091": [0, 1000, 2000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{0, 1000}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists)
				require.Equal(t, "localhost:9091", address)
			},
		},
		{
			name: "address with port and protocol",
			setupViper: func(v *viper.Viper) {
				v.Set("grpc.historical-grpc-address-block-range", `{"https://archive.example.com:9091": [0, 1000]}`)
			},
			expectError: false,
			validate: func(t *testing.T, cfg Config) {
				t.Helper()
				require.Len(t, cfg.GRPC.HistoricalGRPCAddressBlockRange, 1)
				expectedRange := BlockRange{0, 1000}
				address, exists := cfg.GRPC.HistoricalGRPCAddressBlockRange[expectedRange]
				require.True(t, exists)
				require.Equal(t, "https://archive.example.com:9091", address)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			v := viper.New()
			v.Set("minimum-gas-prices", "0stake")
			tt.setupViper(v)
			cfg, err := GetConfig(v)
			if tt.expectError {
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.errorMsg)
			} else {
				require.NoError(t, err)
				if tt.validate != nil {
					tt.validate(t, cfg)
				}
			}
		})
	}
}
